/*
 * Copyright (C) 2020 Grakn Labs
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package grakn.core.graph.graphdb.configuration;

import com.google.common.base.Joiner;
import grakn.core.graph.core.schema.DefaultSchemaMaker;
import grakn.core.graph.diskstorage.StandardIndexProvider;
import grakn.core.graph.diskstorage.configuration.ConfigNamespace;
import grakn.core.graph.diskstorage.configuration.ConfigOption;
import grakn.core.graph.diskstorage.configuration.Configuration;
import grakn.core.graph.diskstorage.configuration.ReadConfiguration;
import grakn.core.graph.diskstorage.idmanagement.ConflictAvoidanceMode;
import grakn.core.graph.diskstorage.util.time.TimestampProvider;
import grakn.core.graph.diskstorage.util.time.TimestampProviders;
import grakn.core.graph.graphdb.tinkerpop.JanusGraphDefaultSchemaMaker;
import grakn.core.graph.graphdb.types.typemaker.DisableDefaultSchemaMaker;
import grakn.core.graph.util.stats.NumberUtil;
import org.apache.commons.configuration.BaseConfiguration;

import java.net.InetAddress;
import java.time.Duration;

/**
 * Provides functionality to configure a JanusGraph INSTANCE.
 * <p>
 * <p>
 * A graph database configuration is uniquely associated with a graph database and must not be used for multiple
 * databases.
 * <p>
 * After a graph database has been initialized with respect to a configuration, some parameters of graph database
 * configuration may no longer be modifiable.
 */
public class GraphDatabaseConfiguration {

    public static final ConfigNamespace ROOT_NS = new ConfigNamespace(null, "root", "Root Configuration Namespace for the JanusGraph Graph Database");

    // ########## Graph-level Config Options ##########
    // ################################################

    public static final ConfigNamespace GRAPH_NS = new ConfigNamespace(ROOT_NS, "graph",
            "General configuration options");

    public static final ConfigOption<TimestampProviders> TIMESTAMP_PROVIDER = new ConfigOption<>(GRAPH_NS, "timestamps",
            "The timestamp resolution to use when writing to storage and indices. Sets the time granularity for the " +
                    "entire graph cluster. To avoid potential inaccuracies, the configured time resolution should match " +
                    "those of the backend systems. Some JanusGraph storage backends declare a preferred timestamp resolution that " +
                    "reflects design constraints in the underlying service. When the backend provides " +
                    "a preferred default, and when this setting is not explicitly declared in the config file, the backend " +
                    "default is used and the general default associated with this setting is ignored.  An explicit " +
                    "declaration of this setting overrides both the general and backend-specific defaults.",
            ConfigOption.Type.FIXED, TimestampProviders.class, TimestampProviders.MICRO);

    public static final ConfigOption<String> UNIQUE_INSTANCE_ID = new ConfigOption<>(GRAPH_NS, "unique-instance-id",
            "Unique identifier for this JanusGraph instance.  This must be unique among all instances " +
                    "concurrently accessing the same stores or indexes.  It's automatically generated by " +
                    "concatenating the hostname, process id, and a static (process-wide) counter. " +
                    "Leaving it unset is recommended.",
            ConfigOption.Type.LOCAL, String.class);

    // ################ Transaction #######################
    // ################################################

    public static final ConfigNamespace TRANSACTION_NS = new ConfigNamespace(ROOT_NS, "tx",
            "Configuration options for transaction handling");

    public static final ConfigOption<Boolean> SYSTEM_LOG_TRANSACTIONS = new ConfigOption<>(TRANSACTION_NS, "LOG-tx",
            "Whether transaction mutations should be logged to JanusGraph's write-ahead transaction LOG which can be used for recovery of partially failed transactions",
            ConfigOption.Type.GLOBAL, false);

    public static final ConfigOption<Duration> MAX_COMMIT_TIME = new ConfigOption<>(TRANSACTION_NS, "max-commit-time",
            "Maximum time (in ms) that a transaction might take to commit against all backends. This is used by the distributed " +
                    "write-ahead LOG processing to determine when a transaction can be considered failed (i.e. after this time has elapsed)." +
                    "Must be longer than the maximum allowed write time.",
            ConfigOption.Type.GLOBAL, Duration.ofSeconds(10));

    // ################ Query Processing #######################
    // ################################################

    public static final ConfigNamespace QUERY_NS = new ConfigNamespace(ROOT_NS, "query",
            "Configuration options for query processing");

    public static final ConfigOption<Boolean> IGNORE_UNKNOWN_INDEX_FIELD = new ConfigOption<>(QUERY_NS, "ignore-unknown-index-key",
            "Whether to ignore undefined types encountered in user-provided index queries",
            ConfigOption.Type.MASKABLE, false);

    public static final String UNKNOWN_FIELD_NAME = "unknown_key";

    public static final ConfigOption<Boolean> PROPERTY_PREFETCHING = new ConfigOption<>(QUERY_NS, "fast-property",
            "Whether to pre-fetch all properties on first singular vertex property access. This can eliminate backend calls on subsequent" +
                    "property access for the same vertex at the expense of retrieving all properties at once. This can be " +
                    "expensive for vertices with many properties",
            ConfigOption.Type.MASKABLE, true);

    public static final ConfigOption<Boolean> ADJUST_LIMIT = new ConfigOption<>(QUERY_NS, "smart-limit",
            "Whether the query optimizer should try to guess a smart limit for the query to ensure responsiveness in " +
                    "light of possibly large result sets. Those will be loaded incrementally if this option is enabled.",
            ConfigOption.Type.MASKABLE, true);

    public static final ConfigOption<Boolean> USE_MULTIQUERY = new ConfigOption<>(QUERY_NS, "batch",
            "Whether traversal queries should be batched when executed against the storage backend. This can lead to significant " +
                    "performance improvement if there is a non-trivial latency to the backend.",
            ConfigOption.Type.MASKABLE, false);

    public static final ConfigOption<Boolean> BATCH_PROPERTY_PREFETCHING = new ConfigOption<>(QUERY_NS, "batch-property-prefetch",
            "Whether to do a batched pre-fetch of all properties on adjacent vertices against the storage backend prior to evaluating a has condition against those vertices. " +
                    "Because these vertex properties will be loaded into the transaction-level cache of recently-used vertices when the condition is evaluated this can " +
                    "lead to significant performance improvement if there are many edges to adjacent vertices and there is a non-trivial latency to the backend.",
            ConfigOption.Type.MASKABLE, false);

    // ################ CACHE #######################
    // ################################################

    public static final ConfigNamespace CACHE_NS = new ConfigNamespace(ROOT_NS, "cache", "Configuration options that modify JanusGraph's caching behavior");

    public static final ConfigOption<Boolean> DB_CACHE = new ConfigOption<>(CACHE_NS, "db-cache",
            "Whether to enable JanusGraph's database-level cache, which is shared across all transactions. " +
                    "Enabling this option speeds up traversals by holding hot graph elements in memory, " +
                    "but also increases the likelihood of reading stale data.  Disabling it forces each " +
                    "transaction to independently fetch graph elements from storage before reading/writing them.",
            ConfigOption.Type.MASKABLE, false);

    /**
     * The size of the database level cache.
     * If this value is between 0.0 (strictly bigger) and 1.0 (strictly smaller), then it is interpreted as a
     * percentage of the total heap space available to the JVM this JanusGraph instance is running in.
     * If this value is bigger than 1.0 it is interpreted as an absolute size in bytes.
     */
    public static final ConfigOption<Double> DB_CACHE_SIZE = new ConfigOption<>(CACHE_NS, "db-cache-size",
            "Size of JanusGraph's database level cache.  Values between 0 and 1 are interpreted as a percentage " +
                    "of VM heap, while larger values are interpreted as an absolute size in bytes.",
            ConfigOption.Type.MASKABLE, 0.3);

    /**
     * How long the database level cache will keep keys expired while the mutations that triggered the expiration
     * are being persisted. This value should be larger than the time it takes for persisted mutations to become visible.
     * This setting only ever makes sense for distributed storage backends where writes may be accepted but are not
     * immediately readable.
     */
    public static final ConfigOption<Integer> DB_CACHE_CLEAN_WAIT = new ConfigOption<>(CACHE_NS, "db-cache-clean-wait",
            "How long, in milliseconds, database-level cache will keep entries after flushing them.  " +
                    "This option is only useful on distributed storage backends that are capable of acknowledging writes " +
                    "without necessarily making them immediately visible.",
            ConfigOption.Type.GLOBAL_OFFLINE, 50);

    /**
     * The default expiration time for elements held in the database level cache. This is the time period before
     * JanusGraph will check against storage backend for a newer query answer.
     * Setting this value to 0 will cache elements forever (unless they get evicted due to space constraints). This only
     * makes sense when this is the only JanusGraph instance interacting with a storage backend.
     */
    public static final ConfigOption<Long> DB_CACHE_TIME = new ConfigOption<>(CACHE_NS, "db-cache-time",
            "Default expiration time, in milliseconds, for entries in the database-level cache. " +
                    "Entries are evicted when they reach this age even if the cache has room to spare. " +
                    "Set to 0 to disable expiration (cache entries live forever or until memory pressure " +
                    "triggers eviction when set to 0).",
            ConfigOption.Type.GLOBAL_OFFLINE, 10000L);

    /**
     * Configures the maximum number of recently-used vertices cached by a transaction. The smaller the cache size, the
     * less memory a transaction can consume at maximum. For many concurrent, long running transactions in memory constraint
     * environments, reducing the cache size can avoid OutOfMemory and GC limit exceeded exceptions.
     * Note, however, that all modifications in a transaction must always be kept in memory and hence this setting does not
     * have much impact on write intense transactions. Those must be split into smaller transactions in the case of memory errors.
     * <p>
     * The recently-used vertex cache can contain both dirty and clean vertices, that is, both vertices which have been
     * created or updated in the current transaction and vertices which have only been read in the current transaction.
     */
    public static final ConfigOption<Integer> TX_CACHE_SIZE = new ConfigOption<>(CACHE_NS, "tx-cache-size",
            "Maximum size of the transaction-level cache of recently-used vertices.",
            ConfigOption.Type.MASKABLE, 20000);

    /**
     * Configures the initial size of the dirty (modified) vertex map used by a transaction.  All vertices created or
     * updated by a transaction are held in that transaction's dirty vertex map until the transaction commits.
     * This option sets the initial size of the dirty map.  Unlike #TX_CACHE_SIZE, this is not a maximum.
     * The transaction will transparently allocate more space to store dirty vertices if this initial size hint
     * is exceeded.  Transactions that know how many vertices they are likely to modify a priori can avoid resize
     * costs associated with growing the dirty vertex data structure by setting this option.
     */
    public static final ConfigOption<Integer> TX_DIRTY_SIZE = new ConfigOption<>(CACHE_NS, "tx-dirty-size",
            "Initial size of the transaction-level cache of uncommitted dirty vertices. " +
                    "This is a performance hint for write-heavy, performance-sensitive transactional workloads. " +
                    "If set, it should roughly match the median vertices modified per transaction.",
            ConfigOption.Type.MASKABLE, Integer.class);

    /**
     * The default value of #TX_DIRTY_SIZE when batch loading is disabled.
     * This value is only considered if the user does not specify a value for
     * {@code #TX_DIRTY_CACHE_SIZE} explicitly in either the graph or transaction config.
     */
    private static final int TX_DIRTY_SIZE_DEFAULT_WITHOUT_BATCH = 32;

    /**
     * The default value of #TX_DIRTY_SIZE when batch loading is enabled.
     * This value is only considered if the user does not specify a value for
     * {@code #TX_DIRTY_CACHE_SIZE} explicitly in either the graph or transaction config.
     */
    private static final int TX_DIRTY_SIZE_DEFAULT_WITH_BATCH = 4096;


    // ################ STORAGE #######################
    // ################################################

    public static final ConfigNamespace STORAGE_NS = new ConfigNamespace(ROOT_NS, "storage", "Configuration options for the storage backend.  Some options are applicable only for certain backends.");

    /**
     * Define the storage backed to use for persistence
     */
    public static final ConfigOption<String> STORAGE_BACKEND = new ConfigOption<>(STORAGE_NS, "backend",
            "The primary persistence provider used by JanusGraph.  This is required.  It should be set one of " +
                    "JanusGraph's built-in shorthand names for its standard storage backends " +
                    "(shorthands: inmemory, cql) " +
                    "or to the full package and classname of a custom/third-party StoreManager implementation.",
            ConfigOption.Type.LOCAL, String.class);

    /**
     * Enables batch loading which improves write performance but assumes that only one thread is interacting with
     * the graph
     */
    public static final ConfigOption<Boolean> STORAGE_BATCH = new ConfigOption<>(STORAGE_NS, "batch-loading",
            "Whether to enable batch loading into the storage backend",
            ConfigOption.Type.LOCAL, false);

    /**
     * Buffers graph mutations locally up to the specified number before persisting them against the storage backend.
     * Set to 0 to disable buffering. Buffering is disabled automatically if the storage backend does not support buffered mutations.
     */
    public static final ConfigOption<Integer> BUFFER_SIZE = new ConfigOption<>(STORAGE_NS, "buffer-size",
            "Size of the batch in which mutations are persisted",
            ConfigOption.Type.MASKABLE, 1024, ConfigOption.positiveInt());

    public static final ConfigOption<Duration> STORAGE_WRITE_WAITTIME = new ConfigOption<>(STORAGE_NS, "write-time",
            "Maximum time (in ms) to wait for a backend write operation to complete successfully. If a backend write operation" +
                    "fails temporarily, JanusGraph will backoff exponentially and retry the operation until the wait time has been exhausted. ",
            ConfigOption.Type.MASKABLE, Duration.ofSeconds(100L));

    public static final ConfigOption<Duration> STORAGE_READ_WAITTIME = new ConfigOption<>(STORAGE_NS, "read-time",
            "Maximum time (in ms) to wait for a backend read operation to complete successfully. If a backend read operation" +
                    "fails temporarily, JanusGraph will backoff exponentially and retry the operation until the wait time has been exhausted. ",
            ConfigOption.Type.MASKABLE, Duration.ofSeconds(10L));

    /**
     * If enabled, JanusGraph attempts to parallelize storage operations against the storage backend using a fixed thread pool shared
     * across the entire JanusGraph graph database instance. Parallelization is only applicable to certain storage operations and
     * can be beneficial when the operation is I/O bound.
     */
    public static final ConfigOption<Boolean> PARALLEL_BACKEND_OPS = new ConfigOption<>(STORAGE_NS, "parallel-backend-ops",
            "Whether JanusGraph should attempt to parallelize storage operations",
            ConfigOption.Type.MASKABLE, true);

    public static final ConfigOption<String[]> STORAGE_HOSTS = new ConfigOption<>(STORAGE_NS, "hostname",
            "The hostname or comma-separated list of hostnames of storage backend servers.  " +
                    "This is only applicable to some storage backends, such as cassandra and hbase.",
            ConfigOption.Type.LOCAL, new String[]{InetAddress.getLoopbackAddress().getHostAddress()});

    /**
     * Configuration key for the port on which to connect to remote storage backend servers.
     */
    public static final ConfigOption<Integer> STORAGE_PORT = new ConfigOption<>(STORAGE_NS, "port",
            "The port on which to connect to storage backend servers.",
            ConfigOption.Type.LOCAL, Integer.class);

    /**
     * Username and password keys to be used to specify an access credential that may be needed to connect
     * with a secured storage backend.
     */
    public static final ConfigOption<String> AUTH_USERNAME = new ConfigOption<>(STORAGE_NS, "username",
            "Username to authenticate against backend",
            ConfigOption.Type.LOCAL, String.class);
    public static final ConfigOption<String> AUTH_PASSWORD = new ConfigOption<>(STORAGE_NS, "password",
            "Password to authenticate against backend",
            ConfigOption.Type.LOCAL, String.class);

    /**
     * Default timeout when connecting to a remote database instance
     * <p>
     */
    public static final ConfigOption<Duration> CONNECTION_TIMEOUT = new ConfigOption<>(STORAGE_NS, "connection-timeout",
            "Default timeout, in milliseconds, when connecting to a remote database instance",
            ConfigOption.Type.MASKABLE, Duration.ofMillis(10000L));

    /**
     * Time in milliseconds for backend manager to wait for the storage backends to
     * become available when JanusGraph is run in server mode. Should the backend manager
     * experience exceptions when attempting to access the storage backend it will retry
     * until this timeout is exceeded.
     * <p>
     * A wait time of 0 disables waiting.
     * <p>
     */
    public static final ConfigOption<Duration> SETUP_WAITTIME = new ConfigOption<>(STORAGE_NS, "setup-wait",
            "Time in milliseconds for backend manager to wait for the storage backends to become available when JanusGraph is run in server mode",
            ConfigOption.Type.MASKABLE, Duration.ofMillis(60000L));

    /**
     * Default number of results to pull over the wire when iterating over a distributed
     * storage backend.
     * This is batch size of results to pull when iterating a result set.
     */
    public static final ConfigOption<Integer> PAGE_SIZE = new ConfigOption<>(STORAGE_NS, "page-size",
            "JanusGraph break requests that may return many results from distributed storage backends " +
                    "into a series of requests for small chunks/pages of results, where each chunk contains " +
                    "up to this many elements.",
            ConfigOption.Type.MASKABLE, 5000);

    public static final ConfigOption<Boolean> DROP_ON_CLEAR = new ConfigOption<>(STORAGE_NS, "drop-on-clear",
            "Whether to drop the graph database (true) or delete rows (false) when clearing storage. " +
                    "Note that some backends always drop the graph database when clearing storage. Also note that indices are " +
                    "always dropped when clearing storage.",
            ConfigOption.Type.MASKABLE, true);

    // ################ STORAGE - META #######################

    public static final ConfigNamespace STORE_META_NS = new ConfigNamespace(STORAGE_NS, "meta", "Meta data to include in storage backend retrievals", true);

    public static final ConfigOption<Boolean> STORE_META_TIMESTAMPS = new ConfigOption<>(STORE_META_NS, "timestamps",
            "Whether to include timestamps in retrieved entries for storage backends that automatically annotated entries with timestamps",
            ConfigOption.Type.GLOBAL, false);

    public static final ConfigOption<Boolean> STORE_META_TTL = new ConfigOption<>(STORE_META_NS, "ttl",
            "Whether to include ttl in retrieved entries for storage backends that support storage and retrieval of cell level TTL",
            ConfigOption.Type.GLOBAL, false);


    // ################ CLUSTERING ###########################
    // ################################################

    public static final ConfigNamespace CLUSTER_NS = new ConfigNamespace(ROOT_NS, "cluster", "Configuration options for multi-machine deployments");

    public static final ConfigOption<Integer> CLUSTER_MAX_PARTITIONS = new ConfigOption<>(CLUSTER_NS, "max-partitions",
            "The number of virtual partition blocks created in the partitioned graph. This should be larger than the maximum expected number of nodes" +
                    " in the JanusGraph graph cluster. Must be greater than 1 and a power of 2.",
            ConfigOption.Type.FIXED, 32, integer -> integer != null && integer > 1 && NumberUtil.isPowerOf2(integer));


    // ################ IDS ###########################
    // ################################################

    public static final ConfigNamespace IDS_NS = new ConfigNamespace(ROOT_NS, "ids", "General configuration options for graph element IDs");

    /**
     * Size of the block to be acquired. Larger block sizes require fewer block applications but also leave a larger
     * fraction of the id pool occupied and potentially lost. For write heavy applications, larger block sizes should
     * be chosen.
     */
    public static final ConfigOption<Integer> IDS_BLOCK_SIZE = new ConfigOption<>(IDS_NS, "block-size",
            "Globally reserve graph element IDs in chunks of this size.  Setting this too low will make commits " +
                    "frequently block on slow reservation requests.  Setting it too high will result in IDs wasted when a " +
                    "graph instance shuts down with reserved but mostly-unused blocks.",
            ConfigOption.Type.GLOBAL_OFFLINE, 10000);

    /**
     * If flush ids is enabled, vertices and edges are assigned ids immediately upon creation. If not, then ids are only
     * assigned when the transaction is committed.
     */
    public static final ConfigOption<Boolean> IDS_FLUSH = new ConfigOption<>(IDS_NS, "flush",
            "When true, vertices and edges are assigned IDs immediately upon creation.  When false, " +
                    "IDs are assigned only when the transaction commits.",
            ConfigOption.Type.MASKABLE, true);

    /**
     * The number of milliseconds that the JanusGraph id pool manager will wait before giving up on allocating a new block
     * of ids. Note, that failure to allocate a new id block will cause the entire database to fail, hence this value
     * should be set conservatively. Choose a high value if there is a lot of contention around id allocation.
     */
    public static final ConfigOption<Duration> IDS_RENEW_TIMEOUT = new ConfigOption<>(IDS_NS, "renew-timeout",
            "The number of milliseconds that the JanusGraph id pool manager will wait before giving up on allocating a new block of ids",
            ConfigOption.Type.MASKABLE, Duration.ofMillis(120000L));

    /**
     * Configures when the id pool manager will attempt to allocate a new id block. When all but the configured percentage
     * of the current block is consumed, a new block will be allocated. Larger values should be used if a lot of ids
     * are allocated in a short amount of time. Value must be in (0,1].
     */
    public static final ConfigOption<Double> IDS_RENEW_BUFFER_PERCENTAGE = new ConfigOption<>(IDS_NS, "renew-percentage",
            "When the most-recently-reserved ID block has only this percentage of its total IDs remaining " +
                    "(expressed as a value between 0 and 1), JanusGraph asynchronously begins reserving another block. " +
                    "This helps avoid transaction commits waiting on ID reservation even if the block size is relatively small.",
            ConfigOption.Type.MASKABLE, 0.3);

    public static final ConfigOption<Integer> CONCURRENT_PARTITIONS = new ConfigOption<>(
            GraphDatabaseConfiguration.IDS_NS, "num-partitions",
            "Number of partition block to allocate for placement of vertices", ConfigOption.Type.MASKABLE, 10);

    // ################ IDAUTHORITY ###################
    // ################################################

    public static final ConfigNamespace IDAUTHORITY_NS = new ConfigNamespace(IDS_NS, "authority", "Configuration options for graph element ID reservation/allocation");

    /**
     * The number of milliseconds the system waits for an id block application to be acknowledged by the storage backend.
     * Also, the time waited after the application before verifying that the application was successful.
     */
    public static final ConfigOption<Duration> IDAUTHORITY_WAIT = new ConfigOption<>(IDAUTHORITY_NS, "wait-time",
            "The number of milliseconds the system waits for an ID block reservation to be acknowledged by the storage backend",
            ConfigOption.Type.GLOBAL_OFFLINE, Duration.ofMillis(300L));

    /**
     * Sets the strategy used by ConsistentKeyIDAuthority to avoid
     * contention in ID block allocation between JanusGraph instances concurrently
     * sharing a single distributed storage backend.
     */
    // This is set to GLOBAL_OFFLINE as opposed to MASKABLE or GLOBAL to prevent mixing both global-randomized and local-manual modes within the same cluster
    public static final ConfigOption<ConflictAvoidanceMode> IDAUTHORITY_CONFLICT_AVOIDANCE = new ConfigOption<>(IDAUTHORITY_NS, "conflict-avoidance-mode",
            "This setting helps separate JanusGraph instances sharing a single graph storage backend avoid contention when reserving ID blocks, " +
                    "increasing overall throughput.",
            ConfigOption.Type.GLOBAL_OFFLINE, ConflictAvoidanceMode.class, ConflictAvoidanceMode.NONE);

    /**
     * When JanusGraph allocates IDs with ConflictAvoidanceMode#GLOBAL_AUTO
     * configured, it picks a random unique ID marker and attempts to allocate IDs
     * from a partition using the marker. The ID markers function as
     * subpartitions with each ID partition. If the attempt fails because that
     * partition + unique id combination is already completely allocated, then
     * JanusGraph will generate a new random unique ID and try again. This controls
     * the maximum number of attempts before JanusGraph assumes the entire partition
     * is allocated and fails the request. It must be set to at least 1 and
     * should generally be set to 3 or more.
     * <p>
     * This setting has no effect when #IDAUTHORITY_CONFLICT_AVOIDANCE is not configured to
     * ConflictAvoidanceMode#GLOBAL_AUTO.
     */
    public static final ConfigOption<Integer> IDAUTHORITY_CAV_RETRIES = new ConfigOption<>(IDAUTHORITY_NS, "randomized-conflict-avoidance-retries",
            "Number of times the system attempts ID block reservations with random conflict avoidance tags before giving up and throwing an exception",
            ConfigOption.Type.MASKABLE, 5);

    /**
     * Configures the number of bits of JanusGraph assigned ids that are reserved for a unique id marker that
     * allows the id allocation to be scaled over multiple sub-clusters and to reduce race-conditions
     * when a lot of JanusGraph instances attempt to allocate ids at the same time (e.g. during parallel bulk loading)
     * <p>
     * IMPORTANT: This should never ever, ever be modified from its initial value and ALL JanusGraph instances must use the
     * same value. Otherwise, data corruption will occur.
     * <p>
     * This setting has no effect when #IDAUTHORITY_CONFLICT_AVOIDANCE is configured to
     * ConflictAvoidanceMode#NONE. However, note that while the
     * conflict avoidance mode can be changed, this setting cannot ever be changed and must therefore be considered a priori.
     */
    public static final ConfigOption<Integer> IDAUTHORITY_CAV_BITS = new ConfigOption<>(IDAUTHORITY_NS, "conflict-avoidance-tag-bits",
            "Configures the number of bits of JanusGraph-assigned element IDs that are reserved for the conflict avoidance tag",
            ConfigOption.Type.FIXED, 4, uniqueIdBitWidth -> uniqueIdBitWidth >= 0 && uniqueIdBitWidth <= 16);

    /**
     * Unique id marker to be used by this JanusGraph instance when allocating ids. The unique id marker
     * must be non-negative and fit within the number of unique id bits configured.
     * By assigning different unique id markers to individual JanusGraph instances it can be assured
     * that those instances don't conflict with one another when attempting to allocate new id blocks.
     * <p>
     * IMPORTANT: The configured unique id marker must fit within the configured unique id bit width.
     * <p>
     * This setting has no effect when #IDAUTHORITY_CONFLICT_AVOIDANCE is configured to
     * ConflictAvoidanceMode#NONE.
     */
    public static final ConfigOption<Integer> IDAUTHORITY_CAV_TAG = new ConfigOption<>(IDAUTHORITY_NS, "conflict-avoidance-tag",
            "Conflict avoidance tag to be used by this JanusGraph instance when allocating IDs",
            ConfigOption.Type.LOCAL, 0);


    // ############## External Index ######################
    // ################################################

    public static final ConfigNamespace INDEX_NS = new ConfigNamespace(ROOT_NS, "index", "Configuration options for the individual indexing backends", true);

    /**
     * Define the indexing backed to use for index support
     */
    public static final ConfigOption<String> INDEX_BACKEND = new ConfigOption<>(INDEX_NS, "backend",
            "The indexing backend used to extend and optimize JanusGraph's query functionality. " +
                    "This setting is optional.  JanusGraph can use multiple heterogeneous index backends.  " +
                    "Hence, this option can appear more than once, so long as the user-defined name between " +
                    "\"" + INDEX_NS.getName() + "\" and \"backend\" is unique among appearances." +
                    "Similar to the storage backend, this should be set to one of " +
                    "JanusGraph's built-in shorthand names for its standard index backends " +
                    "(shorthands: " + Joiner.on(", ").join(StandardIndexProvider.getAllShorthands()) + ") " +
                    "or to the full package and classname of a custom/third-party IndexProvider implementation.",
            ConfigOption.Type.GLOBAL_OFFLINE, "elasticsearch");

    public static final ConfigOption<String> INDEX_DIRECTORY = new ConfigOption<>(INDEX_NS, "directory",
            "Directory to store index data locally",
            ConfigOption.Type.MASKABLE, String.class);

    public static final ConfigOption<String> INDEX_NAME = new ConfigOption<>(INDEX_NS, "index-name",
            "Name of the index if required by the indexing backend",
            ConfigOption.Type.GLOBAL_OFFLINE, "janusgraph");

    public static final ConfigOption<String[]> INDEX_HOSTS = new ConfigOption<>(INDEX_NS, "hostname",
            "The hostname or comma-separated list of hostnames of index backend servers.  " +
                    "This is only applicable to some index backends, such as elasticsearch and solr.",
            ConfigOption.Type.MASKABLE, new String[]{InetAddress.getLoopbackAddress().getHostAddress()});

    public static final ConfigOption<Integer> INDEX_PORT = new ConfigOption<>(INDEX_NS, "port",
            "The port on which to connect to index backend servers",
            ConfigOption.Type.MASKABLE, Integer.class);

    public static final ConfigOption<String> INDEX_CONF_FILE = new ConfigOption<>(INDEX_NS, "conf-file",
            "Path to a configuration file for those indexing backends that require/support a separate config file",
            ConfigOption.Type.MASKABLE, String.class);

    public static final ConfigOption<Integer> INDEX_MAX_RESULT_SET_SIZE = new ConfigOption<>(INDEX_NS, "max-result-set-size",
            "Maximum number of results to return if no limit is specified. For index backends that support scrolling, it represents " +
                    "the number of results in each batch",
            ConfigOption.Type.MASKABLE, 50);

    public static final ConfigOption<Boolean> INDEX_NAME_MAPPING = new ConfigOption<>(INDEX_NS, "map-name",
            "Whether to use the name of the property key as the field name in the index. It must be ensured, that the" +
                    "indexed property key names are valid field names. Renaming the property key will NOT rename the field " +
                    "and its the developers responsibility to avoid field collisions.",
            ConfigOption.Type.GLOBAL, true);


    // ############## Logging System ######################
    // ################################################

    public static final ConfigNamespace LOG_NS = new ConfigNamespace(ROOT_NS, "log", "Configuration options for JanusGraph's logging system", true);

    public static final String MANAGEMENT_LOG = "janusgraph";
    public static final String TRANSACTION_LOG = "tx";
    public static final String USER_LOG = "user";
    public static final String USER_LOG_PREFIX = "ulog_";

    public static final Duration TRANSACTION_LOG_DEFAULT_TTL = Duration.ofDays(7);

    public static final ConfigOption<String> LOG_BACKEND = new ConfigOption<>(LOG_NS, "backend",
            "Define the LOG backed to use",
            ConfigOption.Type.GLOBAL_OFFLINE, "default");

    public static final ConfigOption<Integer> LOG_NUM_BUCKETS = new ConfigOption<>(LOG_NS, "num-buckets",
            "The number of buckets to split LOG entries into for load balancing",
            ConfigOption.Type.GLOBAL_OFFLINE, 1, ConfigOption.positiveInt());

    public static final ConfigOption<Integer> LOG_SEND_BATCH_SIZE = new ConfigOption<>(LOG_NS, "send-batch-size",
            "Maximum number of LOG messages to batch up for sending for logging implementations that support batch sending",
            ConfigOption.Type.MASKABLE, 256, ConfigOption.positiveInt());

    public static final ConfigOption<Integer> LOG_READ_BATCH_SIZE = new ConfigOption<>(LOG_NS, "read-batch-size",
            "Maximum number of LOG messages to read at a time for logging implementations that read messages in batches",
            ConfigOption.Type.MASKABLE, 1024, ConfigOption.positiveInt());

    public static final ConfigOption<Duration> LOG_SEND_DELAY = new ConfigOption<>(LOG_NS, "send-delay",
            "Maximum time in ms that messages can be buffered locally before sending in batch",
            ConfigOption.Type.MASKABLE, Duration.ofMillis(1000L));

    public static final ConfigOption<Duration> LOG_READ_INTERVAL = new ConfigOption<>(LOG_NS, "read-interval",
            "Time in ms between message readings from the backend for this logging implementations that read message in batch",
            ConfigOption.Type.MASKABLE, Duration.ofMillis(5000L));

    public static final ConfigOption<Integer> LOG_READ_THREADS = new ConfigOption<>(LOG_NS, "read-threads",
            "Number of threads to be used in reading and processing LOG messages",
            ConfigOption.Type.MASKABLE, 1, ConfigOption.positiveInt());

    public static final ConfigOption<Duration> LOG_STORE_TTL = new ConfigOption<Duration>(LOG_NS, "ttl",
            "Sets a TTL on all LOG entries, meaning" +
                    "that all entries added to this LOG expire after the configured amount of time. Requires" +
                    "that the LOG implementation supports TTL.",
            ConfigOption.Type.GLOBAL, Duration.class, sd -> null != sd && !sd.isZero());


    //########## KCVSLog Configuration Options #############

    public static final ConfigOption<Duration> LOG_MAX_WRITE_TIME = new ConfigOption<>(LOG_NS, "max-write-time",
            "Maximum time in ms to try persisting LOG messages against the backend before failing.",
            ConfigOption.Type.MASKABLE, Duration.ofMillis(10000L));

    public static final ConfigOption<Duration> LOG_MAX_READ_TIME = new ConfigOption<>(LOG_NS, "max-read-time",
            "Maximum time in ms to try reading LOG messages from the backend before failing.",
            ConfigOption.Type.MASKABLE, Duration.ofMillis(4000L));

    public static final ConfigOption<Duration> LOG_READ_LAG_TIME = new ConfigOption<>(LOG_NS, "read-lag-time",
            "Maximum time in ms that it may take for reads to appear in the backend. If a write does not become" +
                    "visible in the storage backend in this amount of time, a LOG reader might miss the message.",
            ConfigOption.Type.MASKABLE, Duration.ofMillis(500L));

    public static final ConfigOption<Boolean> LOG_KEY_CONSISTENT = new ConfigOption<>(LOG_NS, "key-consistent",
            "Whether to require consistency for LOG reading and writing messages to the storage backend",
            ConfigOption.Type.MASKABLE, false);

    // KCVSLogManager
    public static final ConfigOption<Boolean> LOG_FIXED_PARTITION = new ConfigOption<>(LOG_NS, "fixed-partition",
            "Whether all LOG entries are written to one fixed partition even if the backend store is partitioned." +
                    "This can cause imbalanced loads and should only be used on low volume logs",
            ConfigOption.Type.GLOBAL_OFFLINE, false);

    public static final ConfigOption<Integer> LOG_MAX_PARTITIONS = new ConfigOption<Integer>(LOG_NS, "max-partitions",
            "The maximum number of partitions to use for logging. Setting up this many actual or virtual partitions. Must be bigger than 0 and a power of 2.",
            ConfigOption.Type.FIXED, Integer.class, integer -> integer != null && integer > 0 && NumberUtil.isPowerOf2(integer));

    // ################ Begin Class Definition #######################
    // ###############################################################

    public static final String SYSTEM_PROPERTIES_STORE_NAME = "system_properties";
    public static final String SYSTEM_CONFIGURATION_IDENTIFIER = "configuration";

    private final Configuration configuration;
    private final ReadConfiguration configurationAtOpen;
    private final String uniqueGraphId;
    private final boolean isDistributed;

    private boolean flushIDs;
    private boolean batchLoading;
    private int txVertexCacheSize;
    private int txDirtyVertexSize;
    private DefaultSchemaMaker defaultSchemaMaker;
    private Boolean propertyPrefetching;
    private boolean adjustQueryLimit;
    private Boolean useMultiQuery;
    private Boolean batchPropertyPrefetching;
    private boolean logTransactions;
    private String unknownIndexKeyName;


    public GraphDatabaseConfiguration(ReadConfiguration configurationAtOpen, Configuration configuration, boolean isDistributed) {
        this.configurationAtOpen = configurationAtOpen;
        this.uniqueGraphId = configuration.get(UNIQUE_INSTANCE_ID);
        this.configuration = configuration;
        this.isDistributed = isDistributed;
        this.flushIDs = configuration.get(IDS_FLUSH);
        this.batchLoading = configuration.get(STORAGE_BATCH);

        //Disable auto-type making when batch-loading is enabled since that may overwrite types without warning
        if (batchLoading) {
            defaultSchemaMaker = new DisableDefaultSchemaMaker();
        } else {
            defaultSchemaMaker = new JanusGraphDefaultSchemaMaker();
        }

        this.txVertexCacheSize = configuration.get(TX_CACHE_SIZE);
        //Check for explicit dirty vertex cache size first, then fall back on batch-loading-dependent default
        if (configuration.has(TX_DIRTY_SIZE)) {
            txDirtyVertexSize = configuration.get(TX_DIRTY_SIZE);
        } else {
            txDirtyVertexSize = batchLoading ? TX_DIRTY_SIZE_DEFAULT_WITH_BATCH : TX_DIRTY_SIZE_DEFAULT_WITHOUT_BATCH;
        }

        this.propertyPrefetching = configuration.get(PROPERTY_PREFETCHING);
        this.useMultiQuery = configuration.get(USE_MULTIQUERY);
        this.batchPropertyPrefetching = configuration.get(BATCH_PROPERTY_PREFETCHING);
        this.adjustQueryLimit = configuration.get(ADJUST_LIMIT);
        this.logTransactions = configuration.get(SYSTEM_LOG_TRANSACTIONS);

        this.unknownIndexKeyName = configuration.get(IGNORE_UNKNOWN_INDEX_FIELD) ? UNKNOWN_FIELD_NAME : null;
    }

    public boolean hasFlushIDs() {
        return flushIDs;
    }

    public int getTxVertexCacheSize() {
        return txVertexCacheSize;
    }

    public int getTxDirtyVertexSize() {
        return txDirtyVertexSize;
    }

    public boolean isBatchLoading() {
        return batchLoading;
    }

    public String getUniqueGraphId() {
        return uniqueGraphId;
    }

    public DefaultSchemaMaker getDefaultSchemaMaker() {
        return defaultSchemaMaker;
    }

    public boolean hasPropertyPrefetching() {
        if (propertyPrefetching == null) {
            return isDistributed;
        } else {
            return propertyPrefetching;
        }
    }

    public boolean useMultiQuery() {
        return useMultiQuery;
    }

    public boolean batchPropertyPrefetching() {
        return batchPropertyPrefetching;
    }

    public boolean adjustQueryLimit() {
        return adjustQueryLimit;
    }

    public String getUnknownIndexKeyName() {
        return unknownIndexKeyName;
    }

    public boolean hasLogTransactions() {
        return logTransactions;
    }

    public TimestampProvider getTimestampProvider() {
        return configuration.get(TIMESTAMP_PROVIDER);
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public org.apache.commons.configuration.Configuration getConfigurationAtOpen() {
        BaseConfiguration result = new BaseConfiguration();
        for (String k : configurationAtOpen.getKeys("")) {
            result.setProperty(k, configurationAtOpen.get(k, Object.class));
        }
        return result;
    }
}
